Hopper (Part 1): Getting Things to Work

The Big Idea

This article is for educational purposes only. Note the license agreement (EULA) states that modification of the software is strictly prohibited, however reverse engineering rights are protected by the French copyright laws. I am not to be held accountable for any misfortunes this article brings you.
Many people write cracks and patches but rarely do you see the actual process (as it’s best to keep things a secret). Here, we’ll attempt to reverse engineer the Hopper Disassembler and figure out their license handling technique. I’m currently running Ubuntu 18.04 with Hopper v4.5.11 installed. Throughout this series, we’ll explore different ways to bypass the Hopper license check system and write different working patches (not just PoCs) for each solution.

Steps to Success

Getting it to work first

Alright, let’s get rolling. The first step is to locate the Hopper executable, which is usually at /opt/hopper-v4/bin. Know that the free version of Hopper does not allow you to save files and will display a dialog saying: “You cannot save with the demo version.” The free version also has a session time limit of 30 minutes. Starting up Hopper, we see the license dialog with a button labelled “Try the Demo.” Loading Hopper into IDA and performing a string search reveals the “Try the Demo” string at:

1
.rodata:000000000067219A	0000000D       C	Try the Demo

And following it leads us to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.rodata:0x0000000067217E ; char aLicenseFile[]
.rodata:0x0000000067217E aLicenseFile    db 'License File:',0    ; DATA XREF: sub_506CD0+130↑o
.rodata:0x0000000067217E                                         ; sub_567B90+17↑o
.rodata:0x0000000067218C ; char aValidating[]
.rodata:0x0000000067218C aValidating     db 'Validating...',0    ; DATA XREF: sub_506CD0+1FC↑o
.rodata:0x0000000067219A ; char aTryTheDemo[]
.rodata:0x0000000067219A aTryTheDemo     db 'Try the Demo',0     ; DATA XREF: sub_506CD0+265↑o
.rodata:0x000000006721A7 ; char aBuyALicense[]
.rodata:0x000000006721A7 aBuyALicense    db 'Buy a License',0    ; DATA XREF: sub_506CD0+2CE↑o
.rodata:0x000000006721B5 ; char aOfflineActivat[]
.rodata:0x000000006721B5 aOfflineActivat db 'Offline Activation',0
.rodata:0x000000006721B5                                         ; DATA XREF: sub_506CD0+337↑o
.rodata:0x000000006721B5                                         ; sub_50F120+1AE↑o
.rodata:0x000000006721C8 ; char aValidateLicens[]
.rodata:0x000000006721C8 aValidateLicens db 'Validate License',0 ; DATA XREF: sub_506CD0+3A0↑o

Further following the XREF brings us finally to the function sub_506CD0 which is most likely responsible for showing the license dialog. Therefore, it’s best to rename it to ShowLicenseDialog. Simple logic deduction leads us to believe that this function is run only when a license is not installed/registered with the software.

Setting a breakpoint in GDB and viewing the call stack reveals several functions that were called:

1
2
3
4
5
6
7
8
9
10
11
12
13
───────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────
 ► f 0           506cd0
   f 1           50636e
   f 2           501152
   f 3           638031
   f 4           63f54f
   f 5           568871
   f 6     7ffff7f9f278
   f 7           6380d1
   f 8     7ffff2966122 QObject::event(QEvent*)+226
   f 9     7ffff36f3743 QWidget::event(QEvent*)+2307
   f 10     7ffff3806c4b QMainWindow::event(QEvent*)+347
Breakpoint *0x506CD0

Now we just need to open every address (f1 through f7) in IDA and see what’s there.

We finally found the right one!

Most functions on the call stack are boring, usually Qt function calls and such. However, at 0x638031 we see a spicy one:

1
2
3
4
5
6
7
8
9
10
11
.text:0x00000000638019 loc_638019:                             ; CODE XREF: sub_637FC0+14↑j
.text:0x00000000638019                                         ; sub_637FC0+23↑j
.text:0x00000000638019                 call    sub_504550      ; Possible CheckLicense candidate
.text:0x0000000063801E                 test    al, al
.text:0x00000000638020                 jnz     short loc_63806C
.text:0x00000000638022                 lea     rbx, [rsp+88h+var_88]
.text:0x00000000638026                 mov     rdi, rbx
.text:0x00000000638029                 mov     rsi, r15
.text:0x0000000063802C                 call    sub_501110
.text:0x00000000638031                 mov     rax, [rsp+88h+var_88]
.text:0x00000000638035                 mov     rax, [rax+1A8h]

Note a peculiar pattern: a call to sub_504550 followed by a test instruction and a jnz past the function chain (the chain that eventually leads to ShowLicenseDialog). This is a strong candidate for a common coding pattern:

1
2
3
4
5
6
7
8
9
10
11
uint8_t al = sub_504550();
if(al == 0)
{
    // Has License
    DoStuff();
}
else
{
    // No License
    ShowLicenseDialog();
}

Ah, things are looking good! A closer look at sub_504550 confirms my suspicions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0x00000000504550 sub_504550      proc near               ; CODE XREF: sub_5B7850:loc_5B7896↓p
.text:0x00000000504550                                         ; sub_5ECC10:loc_5ED0BB↓p ...
.text:0x00000000504550                 push    rax
.text:0x00000000504551                 call    sub_5024E0
.text:0x00000000504556                 cmp     eax, 3
.text:0x00000000504559                 jnz     short loc_504565
.text:0x0000000050455B                 call    sub_502E70
.text:0x00000000504560                 call    sub_5024E0      ; Suspicious function call with URL
.text:0x00000000504565
.text:0x00000000504565 loc_504565:                             ; CODE XREF: sub_504550+9↑j
.text:0x00000000504565                 dec     eax
.text:0x00000000504567                 cmp     eax, 2
.text:0x0000000050456A                 setb    al
.text:0x0000000050456D                 pop     rcx
.text:0x0000000050456E                 retn
.text:0x0000000050456E sub_504550      endp

Note the call to sub_502E70 which is a function that references the string “https://www.hopperapp.com/validate_license_v4.php” followed by several network calls and requests. We now know that sub_504550 is a CheckLicense function. Jackpot! Patching the sub_504550 function to always return true (al=1 ) is easy, we can do it like so:

1
2
3
4
.text:0x00000000504550 CheckLicense:                           ; CODE XREF: sub_5B7850:loc_5B7896↓p
.text:0x00000000504550                                         ; sub_5ECC10:loc_5ED0BB↓p ...
.text:0x00000000504550                 mov     al, 1
.text:0x00000000504552                 retn

Good Eyecandy

You may have noticed the horrendous “Demo Version” watermark present in the background of the program. Getting rid of it is as simple as searching for the string “Demo Version” in IDA:

1
.rodata:0x0000000067B380	0000000D	C	Demo Version

A quick patch overwriting the string with all 0’s should do the trick:

1
2
.rodata:0x0000000067B380 ; QString aDemoVersion
.rodata:0x0000000067B380 aDemoVersion    db 0,0,0,0,0,0,0,0,0,0,0,0,0

Now, we’ll attempt to customize the license window. Hopper demo version "About" window

We should search for the string “Demo version” or “Hopper Standard Edition,” as those should appear somewhere near the “About” window dialog code.

1
2
.rodata:0x000000006655B0 ; QString aHopperStandard
.rodata:0x000000006655B0 aHopperStandard db 'Hopper Standard Edition %1',0

Note the following variables declared several bytes further down:

1
2
3
4
5
6
7
8
9
10
.rodata:0x000000006655D8 ; QString aPersonalLicens
.rodata:0x000000006655D8 aPersonalLicens db 'Personal License',0Ah
.rodata:0x000000006655D8                                         ; DATA XREF: PrintLicenseName+41D↑o
.rodata:0x000000006655D8                 db 'Registered to %1',0Ah
.rodata:0x000000006655D8                 db '%2',0Ah
.rodata:0x000000006655D8                 db '(%3)',0
.rodata:0x00000000665602 ; QString aComputerLicens
.rodata:0x00000000665602 aComputerLicens db 'Computer License %1',0Ah
.rodata:0x00000000665602                                         ; DATA XREF: PrintLicenseName+191↑o
.rodata:0x00000000665602                 db 'Registered to %2',0

Again, following the XREF leads us to sub_4C0930 (which we’ll rename to PrintLicenseName). Looking at the graph, we see:

IDA Graph

I’ve labelled the graph this time since it’s quite messy when taken out of context. The pseudocode for this block would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Block 1
...
const char* rdi = "Hopper Standard Edition %1";
int esi = 0x1A;
QString::fromAscii_helper(rdi, esi);
...
// Block 2 via A
int eax = sub_45FC40(); // Returns int in eax
if(eax == 0)
{
    // To loc_4C0CD2 via B
    ...
}
// Block 3 via C
else if(eax == 1)
{
    // Block 4 via D
    ...
    const char* rdi = "Personal License\nRegistered to %1\n%2";
    int esi = 0x29;
    QString::fromAscii_helper(rdi, esi);
    ...
}
// To other case via E
else if(eax == 2)
{
    // Code not shown on graph
    ...
    const char* rdi = "Computer License %1\nRegistered to %2";
    int esi = 0x24;
    QString::fromAscii_helper(rdi, esi);
    ...
}
else
{
    // Code not shown
    ...
}

The following block contains the string aPersonalLicens, which should be the string we want to print to the screen.

1
2
3
4
5
6
7
.text:0x000000004C0D45 loc_4C0D45:                             ; CODE XREF: PrintLicenseName+171↑j
.text:0x000000004C0D45                                         ; PrintLicenseName+17A↑j
.text:0x000000004C0D45                 mov     rax, [r15+30h]
.text:0x000000004C0D49                 mov     r14, [rax+40h]
.text:0x000000004C0D4D                 lea     rdi, aPersonalLicens
.text:0x000000004C0D54                 mov     esi, 29h
.text:0x000000004C0D59                 call    __ZN7QString16fromAscii_helperEPKci ; QString::fromAscii_helper(char const*,int)

Upon further investigation, we see that many calls branch from loc_4C0A9A. Here, we must ensure all branches to loc_4C0D45 are valid with a direct jump (bypassing loc_4C0A9A). When verified, we can just replace the jnz loc_4C0CD2 instruction with a plain jmp loc_4C0A9A, thus bypassing all checks. Then we have the entire aPersonalLicens to ourselves to customize. So, we first patch:

1
.text:0x000000004C0AA1                 jz      loc_4C0CD2

To:

1
.text:0x000000004C0AA1                 jmp     loc_4C0D45

Then override aPersonalLicens to display a custom message. Note the message length of 29h or 41 characters. That is quite restrictive. But also notice that aComputerLicens (which is unused as we skipped over all logical branches leading to its XREF) immediately follows aPersonalLicens, granting us a total of 77 characters to work with (by overwriting aComputerLicens).

Note that after modifying aPersonalLicens, you must change the value stored into esi at 0x4C0D54 to match the message length of your new string. Good luck!

Results

The resultant patched bytes are (copied directly from IDA):

1
2
3
4
5
6
7
8
9
Address             Length  Original bytes                      Patched bytes
00000000004C0AA1    0x4     0F 84 2B 02                         E9 9F 02 00
00000000004C0D55    0x1     29                                  38
0000000000504550    0x6     50 E8 8A DF FF FF                   B0 01 C3 90 90 90
00000000006655D9    0x37    65 72 73 6F 6E 61 6C 20 4C 69 63    61 74 63 68 65 64 20 62 79 20 53
                            65 6E 73 65 0A 52 65 67 69 73 74    6B 65 74 63 68 79 43 61 72 72 6F
                            65 72 65 64 20 74 6F 20 25 31 0A    74 0A 77 77 77 2E 73 6B 65 74 63
                            25 32 0A 28 25 33 29 00 43 6F 6D    68 79 63 61 72 72 6F 74 2E 6E 65
                            70 75 74 65 72 20 4C 69 63 65 6E    6F 63 69 74 69 65 73 2E 6F 72 67

Starting up Hopper, we see that the license dialog is missing and we are allowed to save files unimpeded; therefore confirming a sucessful patch! Yay! :smiley:

Hopper cracked version "About" window

What’s Next?

In part 2, we’ll attempt to write a universal patcher for all Hopper versions. Until then, PEACE OUT.

0%